/*
 * #%L
 * java model
 * %%
 * Copyright (C) 2009 - 2011 prefabware.com Stefan Isele
 * %%
 * All Rights Reserved.
 * This program is dual-licensed under the GNU General Public license
 * or a commercial license.
 * You may select to use one or the other of these licenses.
 * To obtain a commercial license please contact Stefan Isele at www.stefan-isele.de
 * Any modifications to this file must keep this entire header intact.
 * Redistribution, except as permitted by the selected license, is prohibited.
 * #L%
 */
package com.prefabware.jmodel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.prefabware.commons.CollectionUtil;
import com.prefabware.commons.Condition;
import com.prefabware.commons.JavaUtil;
import com.prefabware.commons.QualifiedName;
import com.prefabware.commons.collection.Filter;
import com.prefabware.jmodel.annotation.JAnnotatable;
import com.prefabware.jmodel.annotation.JAnnotation;
import com.prefabware.jmodel.annotation.JAnnotationClass;
import com.prefabware.jmodel.annotation.JAnnotations;
import com.prefabware.jmodel.code.CodeBuffer;
import com.prefabware.jmodel.code.JCodeOptions;
import com.prefabware.jmodel.expression.JExpression;
import com.prefabware.jmodel.expression.JMethodInvocation;
import com.prefabware.jmodel.javadoc.JMultiLineComment;
import com.prefabware.jmodel.javadoc.JTypeJavadoc;
import com.prefabware.jmodel.reference.JReference;
import com.prefabware.jmodel.reference.JReferencing;

/**
 * the base of all types of the Java-Model
 * 
 * @author sisele_job
 * 
 */
public abstract class JType implements JReferencing, JAnnotatable, JExpression {
	/**
	 * the name of the property qualifiedName
	 */
	public static final String PROPERTY_QUALIFIEDNAME="qualifiedName";
	/**
	 * to find a method for a given Set of arguments
	 * 
	 * @author sisele_job
	 * 
	 */
	public class Finder {
		JMethodInvocation find(Set<JArgument> arguments, List<JMethod> methods) {
			for (JMethod method : methods) {
				JMethodInvocation found = new JMethodInvocation(null, null);
				List<JArgument> callerAvailable = new ArrayList<JArgument>();
				List<JArgument> targetAvailable = method.getArguments();
				for (JArgument argument : arguments) {
					if (targetAvailable.contains(argument)) {
						found.bind(argument, null);
						// the callers argument is no longer available
						callerAvailable.remove(argument);
						// the argument is no longer available
						targetAvailable.remove(argument);
					}
				}
				// if all callers arguments are used
				// and all target arguments are filled
				// ,we found a method
				if (callerAvailable.isEmpty() && targetAvailable.isEmpty()) {
					return found;
				}
			}
			// no match
			return null;
		}
	}

	private JAnnotations annotations;
	/**
	 * may be null
	 */
	private JMultiLineComment headerComment;

	private List<JImport> imports;

	private JTypeJavadoc javadoc;

	private Map<JMethodSignature, JMethod> methodMap;

	private JPackage package_;
	private JTypeDeclaration declaration;
	private QualifiedName qualifiedName;

	//TODO qualifiedName should be part of the declaration
	protected JType(QualifiedName qn, JTypeDeclaration declaration) {
		super();
		// reflection returns class[] for arrays. avoid to get this as a
		// classname
		Condition.isFalse(qn.getName().contains("[]"), String.format(
				"name may not contain [], may be %s is an array ?",
				qn.getName()));
		this.imports = new ArrayList<JImport>();
		this.qualifiedName = qn;
		this.declaration = declaration;
		this.declaration.setDeclaringType(this);

		methodMap = new LinkedHashMap<JMethodSignature, JMethod>();
		this.javadoc = new JTypeJavadoc();
		this.annotations = new JAnnotations();
	}

	public void add(JAnnotation element) {
		element.setDeclaringType(this);
		annotations.add(element);
	}

	public JType add(JImport import_) {
		imports.add(import_);
		return this;
	}

	public JType add(JMethod method) {
		Condition.notNull(method, "..method");
		method.setDeclaringType(this);
		JMethodSignature signature = method.getDeclaration().getSignature();
		JMethod existing = this.methodMap.get(signature);
		if (existing == method) {
			// if the same method was allready added before, just ignore it
			return this;
		} else {
			// allow overriding of methods
			//			if (existing != null) {
			//				Condition
			//						.never(Message
			//								.format("cannot add method %s, a method with this signature allready exists : %s",
			//										method.getSignature(),
			//										existing.getSignature()));
			//			}
			methodMap.put(signature, method);

			return this;
		}
	}

	/* 
	 * returns this type, as used in Java code when referencing this type
	 * e.g. <code>java.lang.String</code>
	 * whether simple or quallified name will be used depends on 
	 * the argument options @see JCodeOptions#isUseQualifiedNames
	 */
	@Override
	public String asCode(JCodeOptions options) {
		CodeBuffer buf = new CodeBuffer(options);
		buf.append(this.getQualifiedName());
		buf.setInsertLeadingSpace(false);
		for (int i = 1; i <= this.getDeclaration().getArrayDim(); i++) {
			buf.append("[]");
		}
		return buf.toString();
	}

	public abstract String getJavaKeyword();

	/**
	 * @param cls
	 * @return true if this JType equals the given class without considering
	 *         variableDeclaration parameters
	 */
	public boolean equalsClass(Class<?> cls) {
		return getQualifiedName().getName().equals(cls.getName());
	}

	public JAnnotation getAnnotation(JAnnotationClass annotationClass) {
		return annotations.getAnnotation(annotationClass);
	}

	public JAnnotation getAnnotation(String qualifiedName) {
		return annotations.getAnnotation(qualifiedName);
	}

	public List<JAnnotation> getAnnotations() {
		return annotations.getAnnotations();
	}

	public JMultiLineComment getHeaderComment() {
		return headerComment;
	}

	public List<JImport> getImports() {
		return imports;
	}

	public JTypeJavadoc getJavadoc() {
		return javadoc;
	}

	public JMethod getMethod(JMethodSignature signature) {
		return this.methodMap.get(signature);
	}

	public Map<JMethodSignature, JMethod> getMethodMap() {
		return methodMap;
	}

	public List<JMethod> getMethods() {
		List<JMethod> list = new ArrayList<JMethod>();
		list.addAll(methodMap.values());
		return Collections.unmodifiableList(list);
	}

	/**
	 * @return the simple name of this type
	 */
	public String getName() {
		return this.qualifiedName.getSimpleName();
	}

	public JPackage getPackage() {
		if (package_ == null) {
			package_ = new JPackage(getQualifiedName().getQualifier());
		}
		return package_;
	}

	/**
	 * @return the full qualified name of the class, without type parameters <br>
	 *         examples : <br>
	 *         java.lang.String <br>
	 *         java.util.List
	 */
	public QualifiedName getQualifiedName() {
		return this.qualifiedName;
	}

	

	public List<JReference<?>> getReferences() {
		List<JReference<?>> references = new ArrayList<JReference<?>>();
		for (JMethod method : getMethods()) {
			references.addAll(method.getReferences());
		}
		for (JAnnotation ann : getAnnotations()) {
			references.addAll(ann.getReferences());
		}
		return references;
	}

	@Override
	public String toString() {
		return "JType [declaration=" + declaration + "]";
	}

	/**
	 * @return the name of the source-file for this java-class e.g. String.java
	 */
	public String getSourceFileName() {
		return this.qualifiedName.getSimpleName() + ".java";
	}

	

	public boolean isAnnotationPresent(JAnnotationClass anClass) {
		return annotations.isAnnotationPresent(anClass);
	}

	/**
	 * @return true if this JType is void or Void
	 */
	public boolean isVoid() {
		return (this.equalsClass(void.class) || this.equals(Void.class));
	}

	public boolean isPrimitive() {
		return JavaUtil.PRIMITIVE_NAMES.contains(this.getQualifiedName()
				.getName());
	}

	/**
	 * sets the header comment for the type
	 * 
	 * @param headerComment
	 *            headerComment ==null -> no header headerComment !=null but
	 *            empty -> default header else the headerComment#asCode will be
	 *            used
	 */
	public void setHeaderComment(JMultiLineComment headerComment) {
		this.headerComment = headerComment;
	}

	public void setJavadoc(JTypeJavadoc javadoc) {
		this.javadoc = javadoc;
	}

	public JTypeDeclaration getDeclaration() {
		return declaration;
	}

	/**
	 * @param other
	 * @return a list of references from this type to the other type
	 */
	public List<JReference<?>> getReferences(final JType other) {
		Filter<JReference<?>> referenceByType = new Filter<JReference<?>>() {

			@Override
			public boolean includes(JReference<?> candidate) {
				return candidate.getTo().equals(other);
			}
		};
		return CollectionUtil.filter(this.getReferences(), referenceByType);
	};

	/**
	 * @param other
	 * @return true, if this type references the other type
	 */
	public boolean references(final JType other) {
		return !this.getReferences(other).isEmpty();
	}
}
